/**
 * @(#)Referee.java	
 * 
 * History:	
 * 1.0	30 April 2012
 * 		Added behaviour to start the start race service: StartRaceService
 * 		Added behaviour to receive start race signal from Registration Desk: ReceiveStartRaceSignal
 * 2.0	1 May 2012
 * 		Replace both StartRaceService and ReceiveStartRaceSignal with a new behaviour extending SubscriptionResponder: StartRaceService
 * 2.1  2 May 2012
 * 		Moved from upm.pb4agent.common to upm.pb4agent.team1.platform
 * 		Extend AgBase1 instead of Agent
 * 2.2  7 May 2012
 * 		Add protocol to messages that involve the use of the ontology.
 * 		Added behaviour to receive registration closed signal from Registration Desk: ReceiveRegistrationClosedSignal
 * 3.0	11 May 2012
 * 		Added behaviour to start the race: StartRace
 * 4.0	18 May 2012
 * 		Changed StartRaceService into RefereeService which means referee will handle every service he provides using this one behaviour
 * 		Added a helper class: LocalBoat
 * 		Added behaviour to receive boat ordinate from Boat: ReceiveInformCoordinateMessage
 */
package upm.pb4agent.team1.platform;

import jade.content.Concept;
import jade.content.ContentElement;
import jade.content.lang.Codec.CodecException;
import jade.content.onto.OntologyException;
import jade.content.onto.UngroundedException;
import jade.content.onto.basic.Action;
import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.SimpleBehaviour;
import jade.domain.FIPAException;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import upm.pb4agent.ontology.Dock;
import upm.pb4agent.ontology.EnteringDock;
import upm.pb4agent.ontology.LeavingDock;
import upm.pb4agent.ontology.RaceFinished;
import upm.pb4agent.ontology.RaceStarted;
import upm.pb4agent.ontology.SubscribeReferee;
import upm.pb4agent.team1.AgBase1;
import upm.pb4agent.team1.ontology.InformCoordinate;
import upm.pb4agent.team1.ontology.RegistrationClosed;
import upm.pb4agent.team1.team.Constant;
import upm.pb4agent.team1.team.Constant.DockNumber;
import upm.pb4agent.team1.util.AgentUtil;

/**
 * This class handles the referee functions: (1) To receive subscription for the
 * start race service. All agents that are successfully subscribed to this
 * service will be notified when the race is started. (2) To receive signal from
 * Registration Desk that the race can be started.
 * 
 * @version 4.0
 * @author Bob Nicolaus
 * 
 */
public class Referee extends AgBase1 {

	private static final long serialVersionUID = 4817595559155497110L;

	private boolean isRaceStarted = false;
	private boolean isRaceFinished = false;
	private boolean isRegistrationClosedSignalSent = false;

	private int circuitLength = 100;
	private int boardingExtension = 4;

	private long startingTimePeriod = 5000;
	private long startingTime = 0;
	private long timeStarted = 0;

	private List<LocalBoat> boatList = new ArrayList<LocalBoat>();
	private jade.util.leap.List acceptedTeam = new jade.util.leap.ArrayList();

	// Important points on the race circuit
	private float pointBC = 0; // Dock B
	private float pointCD = 0; // End of dock B
	private float pointDE = 0; // Dock C
	private float pointEF = 0; // End of dock C
	private float pointFG = 0; // Dock D
	private float pointGH = 0; // End of dock D

	// Set of all agents that subscribed to be notified about the race.
	private final Set<AID> subscribers = new HashSet<AID>();

	// Number of boat reaches the finish line
	private int finishBoat = 0;

	@Override
	protected void setup() {
		System.out.println(getLocalName() + ": Enters the system");

		try {

			registerAgent(Constant.REFEREE_DF_TYPE);

			circuitLength = Integer.valueOf(AgentUtil
					.getProperty("circuit_length"));
			boardingExtension = Integer.valueOf(AgentUtil
					.getProperty("boarding_extension"));
			startingTimePeriod = Long.valueOf(AgentUtil
					.getProperty("starting_race_period"));
		} catch (FIPAException e) {
			e.printStackTrace();
		} catch (NumberFormatException e) {
			e.printStackTrace();
		}

		// Identify the points
		pointBC = circuitLength / 4 - boardingExtension / 2;
		pointCD = circuitLength / 4 + boardingExtension / 2;
		pointDE = circuitLength / 2 - boardingExtension / 2;
		pointEF = circuitLength / 2 + boardingExtension / 2;
		pointFG = 3 * circuitLength / 4 - boardingExtension / 2;
		pointGH = 3 * circuitLength / 4 + boardingExtension / 2;

		// Add behaviour
		addBehaviour(new RefereeService(this));
		addBehaviour(new ReceiveRegistrationClosedSignal(this));
		addBehaviour(new StartRace(this));
		addBehaviour(new ReceiveInformCoordinateMessage(this));
	}

	/**
	 * Gets boat from a list based on the boatAID.
	 * 
	 * @param boatAID
	 * @return the boat if exists, null if does not exists
	 */
	private LocalBoat getBoat(AID boatAID) {
		LocalBoat boat = null;

		Iterator<LocalBoat> iterator = boatList.iterator();
		while (iterator.hasNext()) {
			boat = iterator.next();
			if (boatAID.equals(boat.getAgentID())) {
				return boat;
			}
		}

		return null;
	}

	/**
	 * Gets boat position based on the boatAID
	 * 
	 * @param boatAID
	 * @return the boat position if exists, -1 if does not exists
	 */
	private int getBoatPosition(AID boatAID) {
		int position = -1;

		for (int i = 0; i < boatList.size(); i++) {
			if (boatAID.equals(boatList.get(i).getAgentID())) {
				position = i + 1;
				break;
			}
		}

		return position;
	}

	/**
	 * Gets the boat area based on given ordinate.
	 * 
	 * @param ordinate
	 * @return the boat's area
	 */
	private Area getBoatArea(int ordinate) {
		if (ordinate <= pointBC) {
			return Area.b;
		} else if (pointBC < ordinate && ordinate < pointCD) {
			return Area.c;
		} else if (pointCD <= ordinate && ordinate <= pointDE) {
			return Area.d;
		} else if (pointDE < ordinate && ordinate < pointEF) {
			return Area.e;
		} else if (pointEF <= ordinate && ordinate <= pointFG) {
			return Area.f;
		} else if (pointFG < ordinate && ordinate < pointGH) {
			return Area.g;
		} else if (pointGH <= ordinate && ordinate <= circuitLength) {
			return Area.h;
		} else if (circuitLength <= ordinate) {
			return Area.i;
		} else {
			return Area.a;
		}
	}

	/**
	 * This class handles the behaviour of the subscription process. It receives
	 * 2 types of message: SUBSCRIBE and CANCEL. Upon receiving SUBSCRIBE
	 * message, it registers the agent for the service. If the message is a
	 * CANCEL message, it unregisters the agent for the service.
	 * 
	 * @author Bob Nicolaus
	 * 
	 */
	class RefereeService extends SimpleBehaviour {

		private static final long serialVersionUID = 4500881018055153799L;

		public RefereeService(Agent agent) {
			super(agent);
		}

		@Override
		public void action() {

			final MessageTemplate subscribeTemplate = MessageTemplate
					.and(MessageTemplate.and(MessageTemplate.or(MessageTemplate
							.MatchPerformative(ACLMessage.SUBSCRIBE),
							MessageTemplate
									.MatchPerformative(ACLMessage.CANCEL)),
							standardTemplate), MessageTemplate
							.MatchProtocol(Constant.SUBSCRIBE_REFEREE_PROTOCOL));

			ACLMessage incomingMessage = receive(subscribeTemplate);

			if (incomingMessage != null) {
				switch (incomingMessage.getPerformative()) {
				case ACLMessage.SUBSCRIBE:

					// Register the agent
					try {

						ContentElement contentElement = getContentManager()
								.extractContent(incomingMessage);

						if (contentElement instanceof Action) {

							Action action = (Action) contentElement;
							Concept concept = action.getAction();

							if (concept instanceof SubscribeReferee) {
								final AID senderAID = incomingMessage
										.getSender();

								if (subscribers.contains(senderAID)) {

									// Agent has already subscribed before
									replyMessage(incomingMessage,
											ACLMessage.REFUSE,
											Constant.SUBSCRIBE_REFEREE_PROTOCOL);
								} else {

									// Subscribe
									subscribers.add(senderAID);
									replyMessage(incomingMessage,
											ACLMessage.AGREE,
											Constant.SUBSCRIBE_REFEREE_PROTOCOL);
								}
							}
						}
					} catch (UngroundedException e) {
						e.printStackTrace();
					} catch (CodecException e) {
						e.printStackTrace();
					} catch (OntologyException e) {
						e.printStackTrace();
					}

					break;

				case ACLMessage.CANCEL:

					// Unregister the agent
					subscribers.remove(incomingMessage.getSender());
					System.out.println(getLocalName() + ": "
							+ incomingMessage.getSender().getLocalName()
							+ " is removed from the subscriber list.");
					break;

				default:
					break;
				}
			} else {

				block();
			}
		}

		@Override
		public boolean done() {
			return isRaceFinished == true;
		}
	}

	/**
	 * Receives notification from the Registration Desk that the registration
	 * has been closed and the race can be started.
	 * 
	 * @author Morteza Nokhodian
	 * 
	 */
	class ReceiveRegistrationClosedSignal extends SimpleBehaviour {

		private static final long serialVersionUID = -3178916050636570404L;

		public ReceiveRegistrationClosedSignal(Agent agent) {
			super(agent);
		}

		@Override
		public void action() {
			ACLMessage registrationClosedMessage = receive(MessageTemplate
					.and(MessageTemplate.and(standardInternalTemplate,
							MessageTemplate
									.MatchPerformative(ACLMessage.INFORM)),
							MessageTemplate
									.MatchProtocol(Constant.REGISTRATION_CLOSED_PROTOCOL)));

			if (registrationClosedMessage != null) {

				try {

					ContentElement contentElement = getContentManager()
							.extractContent(registrationClosedMessage);

					if (contentElement instanceof Action) {

						Action action = (Action) contentElement;
						Concept concept = action.getAction();

						if (concept instanceof RegistrationClosed) {
							RegistrationClosed registrationClosed = (RegistrationClosed) concept;

							acceptedTeam = registrationClosed.getAcceptedTeam();

							if (acceptedTeam.size() != 0) {
								isRegistrationClosedSignalSent = true;
								startingTime = new Date().getTime();
							} else {
								System.out
										.println(myAgent.getLocalName()
												+ ": No team is registered. Race is canceled!");
								myAgent.doDelete();
							}
						}
					}
				} catch (UngroundedException e) {
					e.printStackTrace();
				} catch (CodecException e) {
					e.printStackTrace();
				} catch (OntologyException e) {
					e.printStackTrace();
				}
			} else {

				block();
			}
		}

		@Override
		public boolean done() {
			return isRegistrationClosedSignalSent == true;
		}
	}

	/**
	 * Handles the message broadcasting to all agents that have been subscribed
	 * previously. Once the registration closed signal is received, the race is
	 * going to start after a certain period stated in the configuration:
	 * starting_race_period.
	 * 
	 * @author Bob Nicolaus
	 * 
	 */
	class StartRace extends SimpleBehaviour {

		private static final long serialVersionUID = 5618681886287007722L;

		private long currentTime;

		public StartRace(Agent agent) {
			super(agent);
		}

		@Override
		public void action() {
			if (isRegistrationClosedSignalSent == true) {

				currentTime = new Date().getTime();

				if (currentTime - startingTimePeriod >= startingTime) {

					// Inform registered agents
					final RaceStarted raceStarted = new RaceStarted();

					for (final AID agentAID : subscribers) {

						Action action = new Action(agentAID, raceStarted);

						try {

							sendMessage(ACLMessage.INFORM, agentAID, action,
									Constant.RACE_STARTED_PROTOCOL);

							System.out.println(getLocalName()
									+ ": Race started message sent to "
									+ agentAID.getLocalName());
						} catch (CodecException e) {
							e.printStackTrace();
						} catch (OntologyException e) {
							e.printStackTrace();
						}
					}

					isRaceStarted = true;
					timeStarted = new Date().getTime();
				} else {

					block(1000);
				}
			}
		}

		@Override
		public boolean done() {
			return isRaceStarted == true;
		}
	}

	/**
	 * Receives boat ordinate
	 * 
	 * @author Morteza Nokhodian
	 * 
	 */
	class ReceiveInformCoordinateMessage extends SimpleBehaviour {

		private static final long serialVersionUID = 6457864509182901533L;

		public ReceiveInformCoordinateMessage(Agent agent) {
			super(agent);
		}

		@Override
		public void action() {
			if (isRaceStarted == true) {
				ACLMessage informCoordinateMessage = receive(MessageTemplate
						.and(MessageTemplate.and(standardInternalTemplate,
								MessageTemplate
										.MatchPerformative(ACLMessage.INFORM)),
								MessageTemplate
										.MatchProtocol(Constant.INFORM_COORDINATE_PROTOCOL)));

				if (informCoordinateMessage != null) {
					try {

						ContentElement contentElement = getContentManager()
								.extractContent(informCoordinateMessage);

						if (contentElement instanceof Action) {

							Action action = (Action) contentElement;
							Concept concept = action.getAction();

							if (concept instanceof InformCoordinate) {
								InformCoordinate informCoordinate = (InformCoordinate) concept;

								System.out.println(myAgent.getLocalName()
										+ ": "
										+ informCoordinateMessage.getSender()
												.getLocalName() + " ("
										+ informCoordinate.getAbscissa() + ", "
										+ informCoordinate.getOrdinate() + ")");

								// Check if boat is in the list
								// If yes then update the position and sort the
								// list. If not then add to the list
								LocalBoat boat = getBoat(informCoordinateMessage
										.getSender());
								if (boat != null) {

									boat.setOrdinate(informCoordinate
											.getOrdinate());
									boat.setAbscissa(informCoordinate
											.getAbscissa());
									Collections.sort(boatList);

									// Update boat Area and check boat
									// coordinate
									checkArea(boat);

									// Print out final result
									if (isRaceFinished == true) {
										printFinalResult();
									}
								} else {

									boatList.add(new LocalBoat(
											informCoordinateMessage.getSender(),
											informCoordinate.getOrdinate()));
								}
							}
						}
					} catch (UngroundedException e) {
						e.printStackTrace();
					} catch (CodecException e) {
						e.printStackTrace();
					} catch (OntologyException e) {
						e.printStackTrace();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				} else {

					block();
				}
			}
		}

		@Override
		public boolean done() {
			return isRaceFinished == true;
		}

		/**
		 * Check if a boat has entered or leaved one area. Send message to the
		 * subscribers related to a specific area.
		 * 
		 * @param boatAID
		 *            The boat agent ID
		 * @param previousArea
		 *            The previous area of the boat based on its last coordinate
		 * @param currentArea
		 *            The current area of the boat based on its current
		 *            coordinate
		 * @throws CodecException
		 * @throws OntologyException
		 */
		private void checkArea(LocalBoat boat) throws CodecException,
				OntologyException {
			Area previousArea = boat.getArea();
			boat.setArea(getBoatArea(boat.getOrdinate()));
			Area currentArea = boat.getArea();

			if (currentArea != Area.b
					&& currentArea.ordinal() - previousArea.ordinal() == 1) {

				System.out.println(myAgent.getLocalName() + ": Boat "
						+ boat.getAgentID().getLocalName()
						+ " is entering area: " + currentArea);

				long currentTime = new Date().getTime();
				Dock dock = null;
				EnteringDock enteringDock = null;
				LeavingDock leavingDock = null;
				Action action = new Action();
				String protocol = null;

				switch (currentArea) {
				case c:

					// send entering second dock message
					dock = new Dock();
					dock.setName(DockNumber.B.toString());

					enteringDock = new EnteringDock();
					enteringDock.setBoatAID(boat.getAgentID());
					enteringDock.setDock(dock);
					enteringDock
							.setPosition(getBoatPosition(boat.getAgentID()));
					enteringDock.setTimeStamp(String.valueOf(currentTime));

					action.setAction(enteringDock);
					protocol = Constant.ENTERING_DOCK_PROTOCOL;
					break;
				case d:

					// send leaving second dock message
					dock = new Dock();
					dock.setName(DockNumber.B.toString());

					leavingDock = new LeavingDock();
					leavingDock.setBoatAID(boat.getAgentID());
					leavingDock.setDock(dock);

					action.setAction(leavingDock);
					protocol = Constant.LEAVING_DOCK_PROTOCOL;
					break;
				case e:

					// send entering third dock message
					dock = new Dock();
					dock.setName(DockNumber.C.toString());

					enteringDock = new EnteringDock();
					enteringDock.setBoatAID(boat.getAgentID());
					enteringDock.setDock(dock);
					enteringDock
							.setPosition(getBoatPosition(boat.getAgentID()));
					enteringDock.setTimeStamp(String.valueOf(currentTime));

					action.setAction(enteringDock);
					protocol = Constant.ENTERING_DOCK_PROTOCOL;
					break;
				case f:

					// send leaving third dock message
					dock = new Dock();
					dock.setName(DockNumber.C.toString());

					leavingDock = new LeavingDock();
					leavingDock.setBoatAID(boat.getAgentID());
					leavingDock.setDock(dock);

					action.setAction(leavingDock);
					protocol = Constant.LEAVING_DOCK_PROTOCOL;
					break;
				case g:

					// send entering fourth dock message
					dock = new Dock();
					dock.setName(DockNumber.D.toString());

					enteringDock = new EnteringDock();
					enteringDock.setBoatAID(boat.getAgentID());
					enteringDock.setDock(dock);
					enteringDock
							.setPosition(getBoatPosition(boat.getAgentID()));
					enteringDock.setTimeStamp(String.valueOf(currentTime));

					action.setAction(enteringDock);
					protocol = Constant.ENTERING_DOCK_PROTOCOL;
					break;
				case h:

					// send leaving fourth dock message
					dock = new Dock();
					dock.setName(DockNumber.D.toString());

					leavingDock = new LeavingDock();
					leavingDock.setBoatAID(boat.getAgentID());
					leavingDock.setDock(dock);

					action.setAction(leavingDock);
					protocol = Constant.LEAVING_DOCK_PROTOCOL;
					break;
				case i:

					boat.setTimeTaken(new Date().getTime() - timeStarted);

					// send race finished dock message
					RaceFinished raceFinished = new RaceFinished();
					raceFinished.setBoatAID(boat.getAgentID());
					raceFinished
							.setPosition(getBoatPosition(boat.getAgentID()));
					raceFinished.setTimeStamp(String.valueOf(currentTime));

					action.setAction(raceFinished);
					protocol = Constant.RACE_FINISHED_PROTOCOL;

					finishBoat = finishBoat + 1;
					if (finishBoat == acceptedTeam.size()) {
						isRaceFinished = true;
					}
					break;
				default:
					break;
				}

				for (final AID agentID : subscribers) {
					action.setActor(agentID);

					sendMessage(ACLMessage.INFORM, agentID, action, protocol);
				}
			}
		}

		/**
		 * Print final result
		 * 
		 * @throws InterruptedException
		 */
		private void printFinalResult() throws InterruptedException {
			Thread.sleep(2000);
			Collections.sort(boatList, new BoatSortByTime());
			System.out
					.println("==================================================");
			System.out
					.println("------------------ Final Result ------------------");
			System.out
					.println("==================================================");
			System.out
					.println("Position Boat          Time             Coordinate");
			for (int i = 0; i < boatList.size(); i++) {
				LocalBoat boat = boatList.get(i);
				System.out
						.println("   "
								+ (i + 1)
								+ ".    "
								+ boat.getAgentID().getLocalName()
								+ "         "
								+ String.format(
										"%d min, %d sec",
										TimeUnit.MILLISECONDS.toMinutes(boat
												.getTimeTaken()),
										TimeUnit.MILLISECONDS.toSeconds(boat
												.getTimeTaken())
												- TimeUnit.MINUTES
														.toSeconds(TimeUnit.MILLISECONDS.toMinutes(boat
																.getTimeTaken())))
								+ "    (" + boat.getAbscissa() + ", "
								+ boat.getOrdinate() + ")");
			}
			System.out
					.println("==================================================");
		}
	}

	/**
	 * A helper class to sort the boats based on its ordinate.
	 * 
	 * @author Morteza Nokhodian
	 * 
	 */
	class LocalBoat implements Comparable<LocalBoat> {

		private AID agentID;
		private int ordinate;
		private int abscissa;
		private Area area;
		private long timeTaken;

		public LocalBoat(AID boatAID, int boatOrdinate) {
			agentID = boatAID;
			ordinate = boatOrdinate;
			area = Area.a;
		}

		public AID getAgentID() {
			return agentID;
		}

		public int getOrdinate() {
			return ordinate;
		}

		public void setOrdinate(int ordinate) {
			this.ordinate = ordinate;
		}

		public int getAbscissa() {
			return abscissa;
		}

		public void setAbscissa(int abscissa) {
			this.abscissa = abscissa;
		}

		public Area getArea() {
			return area;
		}

		public void setArea(Area area) {
			this.area = area;
		}

		public long getTimeTaken() {
			return timeTaken;
		}

		public void setTimeTaken(long timeTaken) {
			this.timeTaken = timeTaken;
		}

		@Override
		public int compareTo(LocalBoat anotherBoat) {
			return Integer.valueOf(ordinate).compareTo(
					Integer.valueOf(anotherBoat.ordinate));
		}
	}

	/**
	 * A helper class to sort the boats based on its time taken to reach finish
	 * line.
	 * 
	 * @author Morteza Nokhodian
	 * 
	 */
	class BoatSortByTime implements Comparator<LocalBoat> {

		@Override
		public int compare(LocalBoat o1, LocalBoat o2) {
			return ((Long) o1.getTimeTaken()).compareTo((Long) o2
					.getTimeTaken());
		}

	}

	/**
	 * Helper to depict the area of the current boat coordinate. a = before the
	 * start line. b= between the start line and the second dock. c = the second
	 * dock. d = between the second dock and the third dock. e = the third dock.
	 * f = between the third dock and the fourth dock. g = the fourth dock. h =
	 * between the fourth dock and the finish line. i = after the finish line.
	 * 
	 * @author Bob Nicolaus
	 * 
	 */
	enum Area {
		a, b, c, d, e, f, g, h, i
	}
}
